// 实现阻塞队列 -- 中间的交易场所的

// 是一个队列 :
// 1 . 生产者放数据到这里 , 消费者从这里拿数据
// 2 . 队列为空时 , 消费者阻塞等待
// 3 . 队列为满时 , 生产者阻塞等待
// 还有实现同步和互斥的关系
// 同步用条件变量

// 防止头文件被重复包含
#pragma once

#include <iostream>
#include <queue>
#include "Cond.hpp"
#include "Mutex.hpp"

namespace BlockQueueModel
{
    // 1 . 展开条件变量命名空间
    using namespace CondModel;
    // 2 . 展开锁的命令空间
    using namespace MutexModel;

    // 设定默认的队列实际容量 , 全局的
    const int gdefaultcap = 5;

    // 实现模版 , 支持在该队列中放任何数据 , 如 : 自定义任务
    template <typename T>
    class BlockQueue
    {
    private:
        // 判空
        bool IsEmpty()
        {
            return _blockq.empty();
        }
        // 判满
        bool IsFull()
        {
            return _blockq.size() == _cap;
        }

    public:
        BlockQueue() {}

        //给生产者生产任务接口 , 即 : 数据入队列接口
        void EnterQueue(T &in)
        {
            // 保护临界资源 , RAII 写法
            LockGuard lockguard(_mutex);
            {
                // 生产者生产数据 , 但是在这里 , 如果队列为满生产者阻塞等待
                while (IsFull())
                {
                    // 走到这里说明来等待了 , 所以休眠生产者线程多了一个
                    _psleep_num++;
                    // 为满了 , 在该条件变量下阻塞等待 , 条件变量一定要用 while 判断
                    _full_cond.Wait(_mutex);
                    // 走到这里说明被唤醒了 , 生产者线程少了一个
                    _psleep_num--;
                }

                // 队列不为满 , 生产者可以生产数据了 : 向队列插入数据 , 走到这里一定有空间
                // 因为队列一定不为满 , 可以继续生产 , 插入数据

                // 1 . 生产数据 , 生产完消费者就知道了 , 队列不为空了
                _blockq.push(in);
            }
            
            // 2 . 如果 , 生产者一直生产 , 消费者所有线程一直处于等待呢 ??
            //     生产者也生产满了 , 也等待 , 二者都等待 , 都什么也干不了
            //     所以 , 为了防止这样的情况 , 我生产一个数据 , 就唤醒一个消费者来把数据拿走处理任务
            
            if (_csleep_num > 0)
            {
                // 唤醒休眠条件下的一个消费者
                _empty_cond.Signal();
                std::cout << " 唤醒了一个消费者 ! " << std::endl;
            }
        }

        //给消费者消费任务接口 , 即 : 出队列接口
        T PopQueue()
        {
            T data;
            // 保护临界资源 , RAII 写法
            LockGuard lockguard(_mutex);
            {
                // 消费者消费数据 , 但是在这里 , 如果队列为空消费者阻塞等待
                while (IsEmpty())
                {
                    // 走到这里说明来等待了 , 所以休眠消费者线程多了一个
                    _csleep_num++;
                    // 为空 , 阻塞等待
                    _empty_cond.Wait(_mutex);
                    // 走到这里说明被唤醒了 , 消费者线程少了一个
                    _csleep_num--;
                }

                // 走到这里 , 队列一定不为空 , 证明一定有数据了 , 消费者可以消费了

                // 1 . 消费者拿数据 , 拿完生产者就知道了 , 队列不为满了
                data = _blockq.front();
                _blockq.pop(); // 拿走了腾出位置
            }

            //2. 唤醒生产者
            if(_psleep_num > 0)
            {
                _full_cond.Signal();
                std::cout << " 唤醒了一个生产者 ! " << std::endl;
            }

            //返回数据
            return data;
        }

        ~BlockQueue()
        {
        }

    private:
        // 阻塞队列就用STL queue 容器实现
        std::queue<T> _blockq;  // 里面放的就是临界资源
        int _cap = gdefaultcap; // 实际容量 , 这里给缺省值

        // 要实现 : 生产者先生产 , 消费者后消费 , 所以线程执行有顺序
        // 所以要实现同步机制 :
        ///// 定义条件变量 , 需要几个条件变量呢 ??
        // 为满时是一个条件 , 要在这个条件下的条件变量等待
        // 为空时是一个条件 , 要在这个条件下的条件变量等待
        // 所以条件有两个 , 就要设置两个条件变量分别在各自条件变量下等待

        Cond _full_cond;  // 为满时的条件变量
        Cond _empty_cond; // 为空时的条件变量

        // 考虑需要该队列需要锁吗 ???
        //  队列中的数据消费者都要访问 , 如果消费者很多呢 ?? 所以需要加锁保护数据(临界资源)
        //  需要几把锁 ??? 只用保护队列数据就行了 , 只有一个队列 , 所以 : 1把
        Mutex _mutex;

        // 加上休眠数量 , 代码更健壮
        int _csleep_num = 0; // 消费者休眠的个数 , 默认为 0
        int _psleep_num = 0; // 生产者休眠的个数 , 默认为 0
    };
}
